package xml.parserSAX;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Diego
 */
public class SAXParser
{
    private boolean ignoreWhiteSpace;
    private ParserState state;
    private Pila<String> pilaNodos;
    private StringBuilder buffer = new StringBuilder();
    private String filename;

    /*
     * Aqui comienza el parser SAX, es una especie de autómata a pila, y una
     * máquina de estados, ejecuta eventos del handler asociado a medida que
     * procesa las lineas leidas del archivo
     */
    public SAXParser(String filename, SAXHandler hnd, boolean ignoreWhiteSpace)
    {
        this.ignoreWhiteSpace = ignoreWhiteSpace;
        this.state = new EstadoInicial(hnd, this);
        this.pilaNodos = new Pila<String>();
        this.filename = filename;
    }

    public void parsing() throws FileNotFoundException, IOException, SAXException
    {
        try
        {
            BufferedReader reader = new BufferedReader(new FileReader(this.filename));

            String linea = null;
            do
            {
                linea = reader.readLine();
                if (linea == null)
                    break;
                else
                    procesar(linea);
            }while(true);
        }
        catch (FileNotFoundException ex)
        {
            Logger.getLogger(SAXParser.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
        catch (IOException ex)
        {
            Logger.getLogger(SAXParser.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
        catch (SAXException ex)
        {
            Logger.getLogger(SAXParser.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
    }

    private void vaciarBuffer()
    {
        this.buffer = new StringBuilder();
    }

    /**
     * Este método toma cada linea leida y se lo pasa al estado actual del parser
     * que es quien determina el evento a llamar del handler
     */
    private void procesar(String linea) throws IOException, SAXException
    {
        StringReader r = new StringReader(linea);
        char c;
        int leido = 0;
        
        for (int i = 0; i != -1; i = r.read())
        {
            c = (char)i;

            if (i == 0)
                continue;

            try
            {
                leido++;
                this.state.procesar(c);
            }
            catch (SAXException ex)
            {
                if (linea.length() < 250)
                {
                    String indicador = "";
                    for (int j = 1; j < leido; j++)
                    {
                        indicador += "_";
                    }
                    indicador += "^";
                    throw new SAXException(SAXException.Tipo.BAD_FORMED, "linea:\n" + linea + "\n" + indicador + "\n\n" + ex.getMessage());
                }
                else
                {
                    throw new SAXException(SAXException.Tipo.BAD_FORMED, "En el caracter "+((Integer)leido).toString()+ " de la linea.\n\n"+ex.getMessage());
                }
            }
        }
    }

    private abstract class ParserState
    {
        protected SAXHandler handler;
        protected SAXParser owner;

        protected static final String CARACTER_INVALIDO = "Documento mal formado. Caracter inválido en el procesamiento para el estado del parser.";

        public ParserState(SAXHandler hnd, SAXParser own)
        {
            handler = hnd;
            owner = own;
        }

        public abstract void procesar(char c) throws SAXException;
    }

    private class EstadoInicial extends ParserState
    {
        public EstadoInicial(SAXHandler hnd, SAXParser own)
        {
            super(hnd, own);
        }

        @Override
        public void procesar(char c) throws SAXException
        {
            if (c == '<')
            {
                this.owner.state = new IniciandoNodo(this.handler, this.owner);
            }
            else if (c == '\n')
            {
                //no se hace nada
            }
            else
            {
                throw new SAXException(SAXException.Tipo.BAD_FORMED,
                        CARACTER_INVALIDO+"\nEstado: Inicial.\nCaracter: "+c);
            }
        }
    }

    private class IniciandoNodo extends ParserState
    {
        private boolean esComentario;

        public IniciandoNodo(SAXHandler hnd, SAXParser own)
        {
            super(hnd, own);
            this.esComentario = false;
        }

        @Override
        public void procesar(char c) throws SAXException
        {
            if (c == '!' & this.owner.buffer.length() == 0)
            {
                this.esComentario = true;
            }
            else if (this.esComentario & c != '>')
            {
                //nada, no se tiene en cuenta
            }
            else if (this.esComentario & c == '>')
            {
                this.owner.vaciarBuffer();

                if (this.owner.pilaNodos.getActual() == null)
                    this.owner.state = new EstadoInicial(handler, owner);
                else
                    this.owner.state = new ProcesandoCaracteres(handler, owner);
            }
            else if(c == '?')
            {
                this.owner.state = new IniciandoInstruccion(handler, owner);
            }
            else if (c == '/')
            {
                this.owner.state = new CerrandoNodo(handler, owner);
            }
            else if (Character.isLetter(c))
            {
                this.owner.buffer.append(c);
                this.owner.state = new ProcesandoNodo(handler, owner);
            }
            else
                throw new SAXException(SAXException.Tipo.BAD_FORMED,
                        CARACTER_INVALIDO+"\nEstado: Iniciando Nodo.\nCaracter: "+c);
        }
    }

    private class IniciandoInstruccion extends ParserState
    {
        public IniciandoInstruccion(SAXHandler hnd, SAXParser own)
        {
            super(hnd, own);
        }

        @Override
        public void procesar(char c) throws SAXException
        {
            if (Character.isLetter(c))
            {
                this.owner.buffer.append(c);
                this.owner.state = new ProcesandoInstruccion(handler, owner);
            }
            else
                throw new SAXException(SAXException.Tipo.BAD_FORMED,
                        CARACTER_INVALIDO+"\nEstado: Iniciando Instruccion.\nCaracter: "+c);
        }
    }

    private class CerrandoNodo extends ParserState
    {
        public CerrandoNodo(SAXHandler hnd, SAXParser own)
        {
            super(hnd, own);
        }

        @Override
        public void procesar(char c) throws SAXException
        {
            if (Character.isLetterOrDigit(c) | ".-_:=".contains(Character.toString(c)))
            {
                this.owner.buffer.append(c);
            }
            else if (c == '>')
            {
                if (this.owner.pilaNodos.getActual().equals(this.owner.buffer.toString()))
                {
                    try
                    {
                        this.owner.vaciarBuffer();
                        String s = this.owner.pilaNodos.popOut();
                        if (this.owner.pilaNodos.getActual() != null)
                        {
                            this.handler.endElement(null, null, s);
                            this.owner.state = new ProcesandoCaracteres(handler, owner);
                        }
                        else
                        {
                            this.handler.endDocument();
                            this.owner.state = new Cerrado(handler, owner);
                        }
                    }
                    catch (Exception ex)
                    {
                        throw new SAXException(SAXException.Tipo.BAD_FORMED, "Archivo mal formado: "+ex.getMessage());
                    }
                }
                else if (this.owner.pilaNodos.getActual() == null)
                {
                    String s = this.owner.buffer.toString();
                    this.owner.pilaNodos.popIn(s);
                    this.handler.startDocument(s);
                    this.owner.vaciarBuffer();
                    this.owner.state = new ProcesandoCaracteres(handler, owner);
                }
                else
                {
                    String s = this.owner.buffer.toString();
                    throw new SAXException(SAXException.Tipo.BAD_FORMED,
                        "Documento mal formado, no se encuentra el elemento: "+s);
                }

            }
            else
                throw new SAXException(SAXException.Tipo.BAD_FORMED,
                        CARACTER_INVALIDO+"\nEstado: Cerrando Nodo.\nCaracter: "+c);
        }
    }

    private class ProcesandoNodo extends ParserState
    {
        private boolean cerrandoNodo;

        public ProcesandoNodo(SAXHandler hnd, SAXParser own)
        {
            super(hnd, own);
            this.cerrandoNodo = false;
        }

        @Override
        public void procesar(char c) throws SAXException
        {
            if ((Character.isLetterOrDigit(c) | Character.isSpaceChar(c) | "@~,;'&[]{}._-:=\\\"#$".contains(Character.toString(c))))
            {
                this.owner.buffer.append(c);
                this.cerrandoNodo = false;
            }
            else if (c == '/')
            {
                this.cerrandoNodo = true;
            }
            else if (c == '>' & !this.cerrandoNodo & this.owner.buffer.length() != 0)
            {
                String s = this.owner.buffer.toString().split(" ")[0];

                if (this.owner.pilaNodos.getActual() == null)
                    this.handler.startDocument(s);
                else
                    this.handler.startElement(s, null);

                this.owner.pilaNodos.popIn(s);
                this.owner.vaciarBuffer();
                this.owner.state = new ProcesandoCaracteres(handler, owner);
            }
            else if (c == '>' & this.cerrandoNodo & this.owner.buffer.length() != 0)
            {
                //No se puede cerrar un nodo cuando la pila de nodos está vacía
                if (this.owner.pilaNodos.getActual().isEmpty())
                {
                    throw new SAXException(SAXException.Tipo.BAD_FORMED,
                            "Se cierra un elemento en un momento inesperado");
                }
                else  //es un elemento unico como <BR/>
                {
                    String s = this.owner.buffer.toString().split(" ")[0];

                    this.handler.startElement(s, null);
                    this.handler.endElement(null, null, s);
                    
                    this.owner.vaciarBuffer();
                    this.owner.state = new ProcesandoCaracteres(handler, owner);
                }
            }
            else
                throw new SAXException(SAXException.Tipo.BAD_FORMED,
                        CARACTER_INVALIDO+"\nEstado: Procesando Nodo.\nCaracter: "+c);
        }
    }

    private class ProcesandoInstruccion extends ParserState
    {
        private boolean cerrandoInstruccion;

        public ProcesandoInstruccion(SAXHandler hnd, SAXParser own)
        {
            super(hnd, own);
            this.cerrandoInstruccion = false;
        }

        @Override
        public void procesar(char c) throws SAXException
        {
            if (Character.isLetterOrDigit(c) | Character.isSpaceChar(c) | "=.-\"/@~,;'&[]{}_:#$".contains(Character.toString(c)))
            {
                this.owner.buffer.append(c);
            }
            else if (c == '?' & this.owner.buffer.length() != 0)
            {
                this.cerrandoInstruccion = true;
            }
            else if (c == '>' & this.cerrandoInstruccion)
            {
                this.handler.processCommand(this.owner.buffer.toString());
                this.owner.vaciarBuffer();
                //las instrucciones solo se pueden procesar al principio del documento
                this.owner.state = new EstadoInicial(handler, owner);
            }
            else
                throw new SAXException(SAXException.Tipo.BAD_FORMED,
                        CARACTER_INVALIDO+"\nEstado: Procesando Instruccion.\nCaracter: "+c);
        }
    }

    private class ProcesandoCaracteres extends ParserState
    {
        public ProcesandoCaracteres(SAXHandler hnd, SAXParser own)
        {
            super(hnd, own);
        }

        @Override
        public void procesar(char c) throws SAXException
        {
            if (Character.isLetterOrDigit(c))
            {
                this.owner.buffer.append(c);
            }
            else if (Character.isSpaceChar(c) | Character.isWhitespace(c))
            {
                if  (this.owner.ignoreWhiteSpace & this.owner.buffer.length() == 0)
                {
                    //no se hace nada, no se agrega
                }
                else
                    this.owner.buffer.append(c);
            }
            else if (c == '<')
            {
                if (this.owner.buffer.length() != 0)
                    this.handler.characters(this.owner.buffer.toString());
                this.owner.vaciarBuffer();
                this.owner.state = new IniciandoNodo(handler, owner);
            }
            else if ("—+*\\\"',;.:-_?&¿=)(/%$#@|!".contains(Character.toString(c)))
                this.owner.buffer.append(c);
            else
                throw new SAXException(SAXException.Tipo.BAD_FORMED,
                        CARACTER_INVALIDO+"\nEstado: Procesando Caracteres.\nCaracter: "+c);
        }
    }

    private class Cerrado extends ParserState
    {
        public Cerrado(SAXHandler hnd, SAXParser own)
        {
            super(hnd, own);
        }

        @Override
        public void procesar(char c) throws SAXException
        {
            throw new SAXException(SAXException.Tipo.BAD_FORMED,
                    "Documento mal formado. No se aceptan caracteres luego de cerrado el documento");
        }

    }

    private class Pila<T>
    {
        private class Nodo
        {
            private T dato;
            private Nodo siguiente;

            public Nodo(T dato)
            {
                this.dato = dato;
            }
        }

        private Nodo puntero;

        public void popIn(T dato)
        {
            Nodo p = new Nodo(dato);

            if (this.puntero == null)
                this.puntero = p;
            else
            {
                p.siguiente = this.puntero;
                this.puntero = p;
            }
        }

        public T popOut() throws Exception
        {
            if (this.puntero == null)
                throw new Exception("Error interno. Estado inconsistente.");
            else
            {
                Nodo p = this.puntero;
                this.puntero = p.siguiente;
                p.siguiente = null;
                return p.dato;
            }
        }

        public T getActual()
        {
            return (this.puntero == null ? null : this.puntero.dato);
        }
    }
}
